<html>
    <head>
        <title>实体和聚合根 Entities & Aggrate Root</title>
    </head>
    <body>
        <script>
            //  实体  Entities

                    /*
                            实体是DDD（领域驱动设计）的核心概念之一。

                            Eric Evans将其描述为“一个不是由其属性从根本上定义的对象，而是由连续性和身份的线程定义的对象”。

                            实体通常映射到关系数据库中的表。
                    */

                    /*
                        一、实体类  Entity Class

                                    实体派生自Entity<TKey>类，如下所示：

                                            public class Book : Entity<Guid>
                                            {
                                                public string Name { get; set; }

                                                public float Price { get; set; }
                                            }

                                    如果不想从基Entity<TKey>类派生实体，可以直接实现IEntity<TKey>接口。

                                    Entity<TKey>类只是使用给定的主键类型定义了一个Id属性，即上面示例中的Guid。
                                        
                                    它可以是其他类型，如string、int、long，或者你需要的任何类型。
                                    
                                    
                                    1、带有 Guid 主键的实体

                                            如果你的实体的Id类型是Guid，则需要实施一些良好的实践：

                                                ①、创建一个构造函数，获取Id作为参数并传递给基类。

                                                    如果你不设置GUID ID，ABP会在保存时设置它，但最好在实体上有一个有效的ID，甚至在将其保存到数据库之前。

                                                ②、如果你使用接受参数的构造函数创建实体，请同时创建private或protected空构造函数。
                                                
                                                    这在你的数据库提供程序从数据库中读取你的实体时使用（在反序列化时）。
                                            
                                                ③、不要使用Guid.NewGuid()来设置Id！在从创建实体的代码传递Id时使用IGuidGenerator服务。

                                                        IGuidGenerator优化以生成顺序GUID，这对于关系数据库中的聚集索引至关重要。

                                            一个实体示例：

                                                    public class Book : Entity<Guid>
                                                    {
                                                        public string Name { get; set; }

                                                        public float Price { get; set; }

                                                        protected Book()
                                                        {

                                                        }

                                                        public Book(Guid id)
                                                        : base(id)
                                                        {

                                                        }
                                                    }

                                            应用程序服务中的用法示例：

                                                    public class BookAppService : ApplicationService, IBookAppService
                                                    {
                                                        private readonly IRepository<Book> _bookRepository;

                                                        public BookAppService(IRepository<Book> bookRepository)
                                                        {
                                                            _bookRepository = bookRepository;
                                                        }

                                                        public async Task CreateAsync(CreateBookDto input)
                                                        {
                                                            await _bookRepository.InsertAsync(
                                                                new Book(GuidGenerator.Create())
                                                                {
                                                                    Name = input.Name,
                                                                    Price = input.Price
                                                                }
                                                            );
                                                        }
                                                    }

                                                    BookAppService为图书实体注入默认的存储库，并使用其InsertAsync方法将Book插入数据库。

                                                    GuidGenerator是IGuidGenerator的类型，它是在ApplicationService基类中定义的属性。GuidGenerator是IGuidGenerator的类型，它是在ApplicationService基类中定义的属性。

                                                    如果你想遵循DDD最佳实践，请参阅下面的聚合示例部分。



                                    2、带有复合主键的实体

                                             某些实体可能需要具有复合键。在这种情况下，你可以从非泛型Entity类派生你的实体。
                                             
                                             例子：

                                                    public class UserRole : Entity // 复合主键提示集成非泛型Entity类
                                                    {
                                                        public Guid UserId { get; set; }

                                                        public Guid RoleId { get; set; }
                                                        
                                                        public DateTime CreationTime { get; set; }

                                                        public UserRole()
                                                        {
                                                                
                                                        }
                                                        
                                                        // 重写GetKeys()方法定义复合主键
                                                        public override object[] GetKeys()
                                                        {
                                                            return new object[] { UserId, RoleId }; // 复合键由UserId和RoleId组成
                                                        }
                                                    }
                                                
                                                对于上面的示例，复合键由UserId和RoleId组成。

                                                对于关系数据库，它是相关表的复合主键。具有复合键的实体应实现如上所示的GetKeys()方法。

                                            ★、请注意，你还需要在对象关系映射（ORM）配置中定义实体的键。例如，请参阅Entity Framework Core集成文档。

                                            ★、请注意，具有复合主键的实体不能使用IRepository<TEntity, TKey>接口，因为它需要单个Id属性。

                                                            但是，你始终可以使用IRepository<TEntity>。


                                    3、Entity.EntityEquals() 
                                                                
                                            Entity.EntityEquals(...)方法用于检查两个实体对象是否相等。

                                            eg:  

                                                Book book1 = ...
                                                Book book2 = ...

                                                if (book1.EntityEquals(book2)) //Check equality
                                                {
                                                    ...
                                                }
                    */

                    /*
                        二、聚合根类 AggregateRoot Class

                                    聚合是域驱动设计中的一种模式。DDD聚合是可以被视为单个单元的域对象集群。

                                    一个示例可能是订单及其行项，它们将是单独的对象，但将订单（及其行项）视为单个聚合很有用。"（请参阅完整描述）

                                    ★、请注意，默认情况下，ABP只为聚合根创建默认存储库。但是，可以包含所有实体。有关详细信息，请参阅存储库留档。

                                    ABP不强制你使用聚合根，你实际上可以使用之前定义的Entity类。

                                    但是，如果你想实现域驱动设计并想创建聚合根类，你可能需要考虑一些最佳实践：

                                           ①、聚合根负责保持其自身的完整性。这对于所有实体也是如此，但聚合根也对其子实体负责。因此，聚合根必须始终处于有效状态。 

                                           ②、聚合根可以由其Id引用。不要通过其导航属性引用它。

                                           ③、聚合根被视为单个单元。它作为单个单元检索和更新。它通常被认为是一个"事务边界"。

                                           ④、在聚合根上使用子实体——不要独立修改它们。

                                    如果你想在应用程序中实现DDD，请参阅实体设计最佳实践指南。


                                    1、聚合示例  Aggregate Example

                                                public class Order : AggregateRoot<Guid>
                                                {
                                                    public virtual string ReferenceNo { get; protected set; }

                                                    public virtual int TotalItemCount { get; protected set; }

                                                    public virtual DateTime CreationTime { get; protected set; }

                                                    public virtual List<OrderLine> OrderLines { get; protected set; }

                                                    protected Order()
                                                    {

                                                    }

                                                    public Order(Guid id, string referenceNo)
                                                    {
                                                        Check.NotNull(referenceNo, nameof(referenceNo));
                                                        
                                                        Id = id;
                                                        ReferenceNo = referenceNo;
                                                        
                                                        OrderLines = new List<OrderLine>();
                                                    }

                                                    public void AddProduct(Guid productId, int count)
                                                    {
                                                        if (count <= 0)
                                                        {
                                                            throw new ArgumentException(
                                                                "You can not add zero or negative count of products!",
                                                                nameof(count)
                                                            );
                                                        }

                                                        var existingLine = OrderLines.FirstOrDefault(ol => ol.ProductId == productId);

                                                        if (existingLine == null)
                                                        {
                                                            OrderLines.Add(new OrderLine(this.Id, productId, count));
                                                        }
                                                        else
                                                        {
                                                            existingLine.ChangeCount(existingLine.Count + count);
                                                        }

                                                        TotalItemCount += count;
                                                    }
                                                }

                                                public class OrderLine : Entity
                                                {
                                                    public virtual Guid OrderId { get; protected set; }

                                                    public virtual Guid ProductId { get; protected set; }

                                                    public virtual int Count { get; protected set; }

                                                    protected OrderLine()
                                                    {

                                                    }

                                                    internal OrderLine(Guid orderId, Guid productId, int count)
                                                    {
                                                        OrderId = orderId;
                                                        ProductId = productId;
                                                        Count = count;
                                                    }

                                                    internal void ChangeCount(int newCount)
                                                    {
                                                        Count = newCount;
                                                    }
                                                    
                                                    // 定义复合主键
                                                    public override object[] GetKeys()
                                                    {
                                                        return new Object[] {OrderId, ProductId};
                                                    }
                                                }

                                            果不想从基AggregateRoot<TKey>类派生聚合根，可以直接实现IAggregateRoot<TKey>接口。

                                            Order是具有GuidtypeId属性的聚合根。它具有OrderLine实体的集合。OrderLine是另一个具有复合主键（OrderId和 ProductId）的实体。 
                                            
                                            然此示例可能无法实现聚合根的所有最佳实践，但它仍然遵循一些良好实践：

                                                ①、 Order有一个公共构造函数，它需要最低要求来构造Order实例。
                                                    
                                                    因此，如果没有id和引用号，就不可能创建订单。protected/私有构造函数只需要在从数据源读取时反序列化对象。

                                                ②、OrderLine构造函数是内部的，因此只允许由域层创建。它在Order.AddProduct方法内部使用。

                                                ③、Order.AddProduct实现将产品添加到订单的业务规则

                                                ④、所有属性都有protectedsetter。这是为了防止实体从实体外部进行任意更改。

                                                            例如，在不向订单添加新产品的情况下设置TotalItemCount是危险的。它的值由AddProduct方法维护。

                                            ABP不会强迫你应用任何DDD规则或模式。但是，当你确实想要应用它们时，它会努力使其成为可能和更容易。留档也遵循相同的原则。


                                    2、复合主键聚合根

                                            虽然聚合根并不常见（也不建议），但实际上可以以与上述实体相同的方式定义复合键。在这种情况下使用非泛型AggregateRoot基类。



                                    3、基础聚合根基类  BasicAggregateRoot 基类

                                            AggregateRoot类实现了IHasExtraProperties和IHasConcurrencyStamp接口，这为派生类带来了两个属性

                                            IHasExtraProperties使实体可扩展（请参阅下面的额外属性部分），
                                            
                                            IHasConcurrencyStamp添加了一个由ABP管理的ConcurrencyStamp属性，以实现乐观并发。

                                            在大多数情况下，这些是聚合根所需的特性。

                                            但是，如果你不需要这些功能，你可以从BasicAggregateRoot<TKey>（或BasicAggregateRoot）继承聚合根
                    */

                    /*
                        三、审计属性的基类和接口  
                        
                                有一些属性，如CreationTime、CreatorId、LastModificationTime…在所有应用程序中都很常见。

                                ABP提供了一些接口和基类来标准化这些属性，并且自动设置它们的值。

                                1、审计接口  Auditing Intreface  

                                                        有很多审计接口，因此你可以实现你需要的接口。

                                                        虽然你可以手动实现这些接口，但你可以使用下一节中定义的基类来简化它。


                                                        ①、IHasCreationTime定义了以下属性：
                                                                                
                                                                        CreationTime

                                                        ②、IMayHaveCreator定义了以下属性：
                    
                                                                        CreatorId

                                                        ③、ICreationAuditedObject继承自IHasCreationTime和IMayHaveCreator，因此它定义了以下属性：

                                                                        CreationTime
                                                                        CreatorId

                                                        ④、IHasModificationTime定义了以下属性：
                                                
                                                                        LastModificationTime

                                                        ⑤、IModificationAuditedObject扩展了IHasModificationTime并添加了LastModifierId属性。因此，它定义了以下属性：

                                                                        LastModificationTime
                                                                        LastModifierId

                                                        ⑥、IAuditedObject扩展了ICreationAuditedObject和IModificationAuditedObject，因此它定义了以下属性：

                                                                        CreationTime
                                                                        CreatorId
                                                                        LastModificationTime
                                                                        LastModifierId

                                                        ⑦、ISoftDelete（参见数据过滤文档）定义了以下属性：

                                                                        IsDeleted

                                                        ⑧、IHasDeletionTime扩展了ISoftDelete并添加了DeletionTime属性。因此，它定义了以下属性：

                                                                        IsDeleted
                                                                        DeletionTime

                                                        ⑨、IDeletionAuditedObject扩展了IHasDeletionTime并添加了DeleterId属性。因此，它定义了以下属性：

                                                                        IsDeleted
                                                                        DeletionTime
                                                                        DeleterId

                                                        ⑩、IFullAuditedObject继承自IAuditedObject和IDeletionAuditedObject，因此它定义了以下属性：

                                                                        CreationTime
                                                                        CreatorId
                                                                        LastModificationTime
                                                                        LastModifierId
                                                                        IsDeleted
                                                                        DeletionTime
                                                                        DeleterId

                                                一旦你实现了任何接口，或者从下一节中定义的类派生，ABP就会尽可能自动管理这些属性。



                               2、审计基类  Auditing Base Class

                                                
                                          虽然你可以手动实现上面定义的任何接口，但建议从此处定义的基类继承：
                                          
                                                ①、CreationAuditedEntity<TKey>和CreationAuditedAggregateRoot<TKey>实现ICreationAuditedObject接口。

                                                ②、AuditedEntity<TKey>和AuditedAggregateRoot<TKey>实现IAuditedObject接口。

                                                ③、FullAuditedEntity<TKey>和FullAuditedAggregateRoot<TKey>实现IFullAuditedObject接口。

                                         所有这些基类也有非泛型版本，以采用AuditedEntity和FullAuditedAggregateRoot来支持复合主键。


                                         所有这些基类也有...WithUser对，如FullAuditedAggregateRootWithUser<TUser>和FullAuditedAggregateRootWithUser<TKey, TUser>。
                                        
                                        这使得向你的用户实体添加导航属性成为可能。

                                        但是，在聚合根之间添加导航属性不是一个好的做法，
                                        
                                        因此不建议使用这种用法（除非你使用的是ORM，如EF Core，它很好地支持这种情况并且你真的需要它——否则请记住，这种方法不适用于MongoDB等非关系型数据库，你必须真正实现聚合模式）。

                                        此外，如果你将导航属性添加到启动模板随附的AppUser类，请考虑在迁移数据库上下文中处理（忽略/映射）它（请参阅EF Core迁移文档）。
                    */

                    /*
                        四、实体缓存

                                ABP为缓存实体提供了一个分布式实体缓存系统。
                                        
                                如果你想使用缓存来更快地访问实体而不是从数据库中重复查询它，它很有用。

                                它被设计为只读，如果实体被更新或删除，它会自动使缓存的实体无效。

                                有关详细信息，请参阅“”实体缓存文档"。
                    */

                    /*
                        五、版本控制实体

                                ABP定义了IHasEntityVersion接口，用于实体的自动版本控制。

                                它只提供一个EntityVersion属性，如以下代码块所示：

                                            public interface IHasEntityVersion
                                            {
                                                int EntityVersion { get; }
                                            } 

                                如果你实现IHasEntityVersion接口，ABP会在你更新实体时自动增加EntityVersion值。

                                当你第一次创建实体并保存到数据库时，初始EntityVersion值将为0。

                                备注：

                                        果你直接在数据库中执行SQLUPDATE命令，ABP无法增加版本。

                                        在这种情况下，你有责任增加EntityVersion值。


                                        此外，如果你使用聚合模式并更改聚合根的子集合，如果你想增加聚合根对象的版本，你有责任。
                    */

                    /*
                        六、额外属性

                                ABP定义了IHasExtraProperties接口，该接口可以由实体实现，以便能够动态设置和获取实体的属性。

                                AggregateRoot基类已经实现了IHasExtraProperties接口。

                                如果你是从这个类派生的（或者上面定义的相关审计类之一），你可以直接使用API。


                                1、GetProperty 和  SetProperty 扩展方法

                                            这些扩展方法时获取和设置实体数据的推荐方法。

                                            eg：

                                                // 声明一个额外属性服务
                                                public class ExtraPropertiesDemoService : ITransientDependency
                                                {
                                                    private readonly IIdentityUserRepository _identityUserRepository;

                                                    public ExtraPropertiesDemoService(IIdentityUserRepository identityUserRepository)
                                                    {
                                                        _identityUserRepository = identityUserRepository;
                                                    }

                                                    public async Task SetTitle(Guid userId, string title)
                                                    {
                                                        var user = await _identityUserRepository.GetAsync(userId);
                                                        
                                                        // 设置属性值
                                                        user.SetProperty("Title", title);
                                                        
                                                        await _identityUserRepository.UpdateAsync(user);
                                                    }

                                                    public async Task<string> GetTitle(Guid userId)
                                                    {
                                                        var user = await _identityUserRepository.GetAsync(userId);

                                                        // 获取属性值
                                                        return user.GetProperty<string>("Title");
                                                    }
                                                }

                                            属性的值是object，可以是任何类型的对象（字符串、int、bool…等）。

                                            GetProperty如果之前未设置给定属性，则返回null。

                                            你可以使用不同的属性名称同时存储多个属性（如此处的Title）


                                            为属性名称定义一个常量以防止错别字错误将是一种很好的做法。

                                            定义扩展方法以利用智能感知甚至是一种更好的做法。
                                            
                                            例子：

                                                // 扩展IdentityUser 类
                                                public static class IdentityUserExtensions
                                                {
                                                    // 定义常量作为属性名，防止写错
                                                    private const string TitlePropertyName = "Title";

                                                    // 定义属性设置方法
                                                    public static void SetTitle(this IdentityUser user, string title)
                                                    {
                                                        user.SetProperty(TitlePropertyName, title);
                                                    }

                                                    // 定义获取属性的方法
                                                    public static string GetTitle(this IdentityUser user)
                                                    {
                                                        return user.GetProperty<string>(TitlePropertyName);
                                                    }
                                                }

                                            后你可以直接将user.SetTitle("...")和user.GetTitle()用于IdentityUser对象。


                                2、HasProperty和RemoveProperty扩展方法
                                
                                            HasProperty用于检查对象之前是否设置了属性。

                                            RemoveProperty用于从对象中删除属性。你可以使用它而不是设置null值
                                        
                                             Create


                                3、额外属性是如何实现的？

                                            IHasExtraProperties接口需要为实现的类定义一个名为ExtraProperties的Dictionary<string, object>属性。

                                            因此，如果你愿意，你可以直接使用ExtraProperties属性来使用字典API。

                                            但是，SetProperty和GetProperty方法是推荐的方法，因为它们还检查nulls。


                                            ①、额外属性是如何存储的？

                                                    将此字典存储在数据库中的方式取决于你使用的数据库提供程序。

                                                    对于Entity Framework Core，这里有两种类型的配置：

                                                        （1）、默认情况下，它作为JSON字符串存储在单个ExtraProperties字段中
                                                               
                                                               （这意味着所有额外的属性都存储在单个数据库表字段中）。


                                                        （2）、如果需要，可以使用ObjectExtensionManager为所需的额外属性定义单独的表字段。

                                                               未通过ObjectExtensionManager配置的属性将继续使用如上所述的单个JSON字段。

                                                               当你正在使用预构建的应用程序模块并希望扩展其实体时，此功能特别有用。

                                                               请参阅EF Core集成文档以了解如何使用ObjectExtensionManager。


                                                    对于MongoDB，它被存储为常规字段，因为MongoDB自然支持这种额外元素系统。


                                 4、关于额外属性的讨论

                                        如果你使用定义内部实体的可重用模块并且你希望以简单的方式获取/设置与该实体相关的一些数据，
                                        
                                        则Extra Properties系统特别有用。

                                        你通常不需要将此系统用于你自己的实体，因为它具有以下缺点：

                                                它是不是完全类型安全的，因为它使用字符串作为属性名称。

                                                自动将这些属性从/映射到其他对象并不容易。


                                        除了实体，其他任何类只要实现了 IHasExtraProperties，你就可以为任何类搞额外属性的扩展。      
                    */
        </script>
    </body>
</html>